package org.apache.solr.ocean.search;

import org.apache.lucene.index.FilterIndexReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.ExtendedFieldCache;
import org.apache.lucene.search.FieldDoc;
import org.apache.lucene.search.FieldDocSortedHitQueue;
import org.apache.lucene.search.FieldSortedHitQueue;
import org.apache.lucene.search.HitCollector;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.Weight;
import org.apache.solr.ocean.cache.DocSetCacheKey;
import org.apache.solr.ocean.core.Index.IndexSnapshot;
import org.apache.solr.ocean.util.Constants;
import org.apache.solr.ocean.util.DistanceUtils;
import org.apache.solr.ocean.util.MutableInt;
import org.apache.solr.ocean.util.Timeout;
import org.apache.solr.ocean.util.Timeout.TimeoutRuntimeException;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.search.DocSet;

public class IndexSnapshotSearch extends Search {
  private final IndexSnapshot indexSnapshot;
  private int numDocs;
  private final Sort sort;
  private final Weight weight;
  private final Timeout timeout;
  private final IndexSchema indexSchema;
  private final DistanceOperation distanceOperation;
  private final DocSet filterDocSet;
  private DocSet queryDocSet;

  public IndexSnapshotSearch(SearchQuery searchQuery, IndexSnapshot indexSnapshot) throws Exception {
    super(searchQuery);
    this.indexSnapshot = indexSnapshot;
    indexSchema = indexSnapshot.getIndexSchema();
    weight = searchQuery.getWeight();
    numDocs = searchQuery.getNumDocs();
    sort = searchQuery.getSort();
    timeout = searchQuery.getTimeout();
    Filters filters = searchQuery.getFilters();
    distanceOperation = getDistanceOperation(filters, indexSnapshot);
    filterDocSet = getFilterDocSet(filters, timeout);
  }

  public Result call() throws Exception {
    if (searchQuery.hasSort()) {
      SearchBySort searchBySort = new SearchBySort();
      return searchBySort.search();
    } else {
      SearchByScore searchByScore = new SearchByScore();
      return searchByScore.search();
    }
  }

  /**
   * makes doc set seem deleted
   */
  public static class SearchIndexReader extends FilterIndexReader {
    private DocSet docSet;
    private Timeout timeout;

    public SearchIndexReader(IndexReader indexReader, DocSet docSet, Timeout timeout) {
      super(indexReader);
      if (docSet == null)
        throw new IllegalArgumentException("docSet null");
      this.docSet = docSet;
      this.timeout = timeout;
    }

    public boolean isDeleted(int doc) {
      if (timeout != null && !timeout.isOK()) {
        throw new TimeoutRuntimeException("", timeout);
      }
      if (in.isDeleted(doc))
        return true;
      if (docSet != null) {
        return !docSet.exists(doc);
      } else {
        return true;
      }
    }

    public boolean hasDeletions() {
      return true;
    }
  }

  private class SearchBySort {
    private OceanDoc reusableFD;
    Double docDistanceMiles = null;
    
    public Result search() throws Exception {
      long startTime = Timeout.TIMER_THREAD.getTime();
      final MutableInt numFound = new MutableInt(0);
      final FieldSortedHitQueue queue = new FieldSortedHitQueue(indexSnapshot.getIndexReader(), sort.getSort(), numDocs);// sort,
      IndexReader snapshotIndexReader = indexSnapshot.getIndexReader();
      IndexReader indexReader = new SearchIndexReader(snapshotIndexReader, filterDocSet, timeout);
      Scorer scorer = weight.scorer(indexReader);
      scorer.score(new HitCollector() {
        public void collect(int doc, float score) {
          if (score > 0.0f) {
            numFound.value++;
            if (reusableFD == null) {
              if (distanceOperation != null) {
                docDistanceMiles = distanceOperation.getDocDistance(doc);
                if (docDistanceMiles == null)
                  return;
              }
              reusableFD = new OceanDoc(doc, score, docDistanceMiles);
              docDistanceMiles = null;
            } else {
              reusableFD.score = score;
              reusableFD.doc = doc;
            }
            reusableFD = (OceanDoc) queue.insertWithOverflow(reusableFD);
          }
        }
      });
      float maxScore = queue.getMaxScore();
      ScoreDoc[] scoreDocs = toArray(queue);
      for (ScoreDoc scoreDoc : scoreDocs) {
        FieldDoc fieldDoc = (FieldDoc) scoreDoc;
        queue.fillFields(fieldDoc);
      }
      long elapsed = Timeout.TIMER_THREAD.getTime() - startTime;
      return new Result(searchQuery.getStart(), scoreDocs, (int) numFound.value, elapsed, maxScore);
    }
  }

  private class SearchByScore {
    float minScore;
    private OceanDoc reusableSD;
    private Double docDistanceMiles = null;

    public SearchByScore() {
    }

    public Result search() throws Exception {
      long startTime = Timeout.TIMER_THREAD.getTime();
      minScore = Float.NEGATIVE_INFINITY;
      final MutableInt numFound = new MutableInt(0);
      final FieldDocSortedHitQueue queue = new FieldDocSortedHitQueue(new SortField[] { SortField.FIELD_SCORE }, numDocs);
      IndexReader snapshotIndexReader = indexSnapshot.getIndexReader();
      IndexReader indexReader = new SearchIndexReader(snapshotIndexReader, filterDocSet, timeout);
      Scorer scorer = weight.scorer(indexReader);
      scorer.score(new HitCollector() {
        public void collect(int doc, float score) {
          if (score > 0.0f) {
            numFound.value++;
            if (reusableSD == null) {
              if (distanceOperation != null) {
                docDistanceMiles = distanceOperation.getDocDistance(doc);
                if (docDistanceMiles == null)
                  return;
              }
              reusableSD = new OceanDoc(doc, score, docDistanceMiles);
              docDistanceMiles = null;
            } else if (score >= reusableSD.score) {
              reusableSD.doc = doc;
              reusableSD.score = score;
            } else {
              return;
            }
            reusableSD = (OceanDoc) queue.insertWithOverflow(reusableSD);
          }
        }
      });
      ScoreDoc[] scoreDocs = toArray(queue);
      float maxScore = (numFound.value == 0) ? Float.NEGATIVE_INFINITY : scoreDocs[0].score;
      long elapsed = Timeout.TIMER_THREAD.getTime() - startTime;
      return new Result(searchQuery.getStart(), scoreDocs, (int) numFound.value, elapsed, maxScore);
    }
  }

  private static DistanceOperation getDistanceOperation(Filters filters, IndexSnapshot indexSnapshot) throws Exception {
    if (filters != null && filters.hasDistance()) {
      DistanceOperation distanceOperation = new DistanceOperation();
      distanceOperation.distance = filters.getDistance();
      IndexReader indexReader = indexSnapshot.getIndexReader();
      double[] latitudeArray = ExtendedFieldCache.EXT_DEFAULT.getDoubles(indexReader, Constants.LATITUDE);
      double[] longitudeArray = ExtendedFieldCache.EXT_DEFAULT.getDoubles(indexReader, Constants.LONGITUDE);
      return distanceOperation;
    }
    return null;
  }

  private DocSet getFilterDocSet(Filters filters, Timeout timeout) throws Exception {
    if (filters == null || filters.size() == 0)
      return null;
    DocSetCacheKey cacheKey = filters.getCacheKey();
    if (cacheKey == null)
      return null;
    return indexSnapshot.getDocSet(cacheKey, timeout);
  }

  private static class DistanceOperation {
    public Distance distance;
    public double[] latitudeArray;
    public double[] longitudeArray;

    public Double getDocDistance(int doc) {
      double docDistance = getDistanceMiles(doc);
      if (docDistance > distance.getDistanceMiles())
        return null;
      return docDistance;
    }

    public double getDistanceMiles(int doc) {
      return createDistanceCurved(latitudeArray[doc], longitudeArray[doc], distance);
    }
  }

  public static double createDistanceCurved(Double latitude, Double longitude, Distance distance) {
    return DistanceUtils.getDistanceMi(distance.getLatitude(), distance.getLongitude(), latitude.doubleValue(), longitude.doubleValue());
  }
}
